// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.activities

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivityUserDataBinding
import org.dolphinemu.dolphinemu.dialogs.NotificationDialog
import org.dolphinemu.dolphinemu.dialogs.TaskDialog
import org.dolphinemu.dolphinemu.dialogs.UserDataImportWarningDialog
import org.dolphinemu.dolphinemu.features.DocumentProvider
import org.dolphinemu.dolphinemu.model.TaskViewModel
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import org.dolphinemu.dolphinemu.utils.*
import org.dolphinemu.dolphinemu.utils.ThemeHelper
import org.dolphinemu.dolphinemu.utils.ThemeHelper.enableScrollTint
import org.dolphinemu.dolphinemu.utils.ThemeHelper.setCorrectTheme
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import kotlin.system.exitProcess

class UserDataActivity : AppCompatActivity(), ThemeProvider {
    private lateinit var taskViewModel: TaskViewModel

    private lateinit var mBinding: ActivityUserDataBinding

    override var themeId: Int = 0
    private val requestImport = registerForActivityResult(
        ActivityResultContracts.OpenDocument()
    ) { uri: Uri? ->
        if (uri != null) {
            val arguments = Bundle()
            arguments.putString(
                UserDataImportWarningDialog.KEY_URI_RESULT,
                uri.toString()
            )

            val dialog = UserDataImportWarningDialog()
            dialog.arguments = arguments
            dialog.show(supportFragmentManager, UserDataImportWarningDialog.TAG)
        }
    }

    private val requestExport = registerForActivityResult(
        ActivityResultContracts.CreateDocument("application/zip")
    ) { uri: Uri? ->
        if (uri != null) {
            taskViewModel.clear()
            taskViewModel.task = { exportUserData(uri) }

            val arguments = Bundle()
            arguments.putInt(TaskDialog.KEY_TITLE, R.string.export_in_progress)
            arguments.putInt(TaskDialog.KEY_MESSAGE, 0)
            arguments.putBoolean(TaskDialog.KEY_CANCELLABLE, true)

            val dialog = TaskDialog()
            dialog.arguments = arguments
            dialog.show(supportFragmentManager, TaskDialog.TAG)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        taskViewModel = ViewModelProvider(this)[TaskViewModel::class.java]

        ThemeHelper.setTheme(this)
        enableEdgeToEdge()

        super.onCreate(savedInstanceState)

        mBinding = ActivityUserDataBinding.inflate(layoutInflater)
        setContentView(mBinding.root)

        val android7 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
        val android10 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
        val android11 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
        val legacy = DirectoryInitialization.isUsingLegacyUserDirectory()

        val userDataNewLocation =
            if (android10) R.string.user_data_new_location_android_10 else R.string.user_data_new_location
        mBinding.textType.setText(if (legacy) R.string.user_data_old_location else userDataNewLocation)

        val openFileManagerStringId =
            if (android7) R.string.user_data_open_user_folder else R.string.user_data_open_system_file_manager
        mBinding.buttonOpenSystemFileManager.setText(openFileManagerStringId)

        mBinding.textPath.text = DirectoryInitialization.getUserDirectory()

        mBinding.textAndroid11.visibility = if (android11 && !legacy) View.VISIBLE else View.GONE

        mBinding.buttonOpenSystemFileManager.visibility = if (android11) View.VISIBLE else View.GONE
        mBinding.buttonOpenSystemFileManager.setOnClickListener { openFileManager() }

        mBinding.buttonImportUserData.setOnClickListener { importUserData() }

        mBinding.buttonExportUserData.setOnClickListener { exportUserData() }

        mBinding.buttonResetSettings.setOnClickListener { confirmResetSettings() }

        setSupportActionBar(mBinding.toolbarUserData)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)

        setInsets()
        enableScrollTint(this, mBinding.toolbarUserData, mBinding.appbarUserData)

        if (savedInstanceState == null) {
            Analytics.checkAnalyticsInit(this)
        }
    }

    override fun onSupportNavigateUp(): Boolean {
        onBackPressed()
        return true
    }

    override fun onResume() {
        setCorrectTheme(this)
        super.onResume()
    }

    override fun setTheme(themeId: Int) {
        super.setTheme(themeId)
        this.themeId = themeId
    }

    private fun confirmResetSettings() {
        MaterialAlertDialogBuilder(this)
            .setTitle(R.string.reset_dolphin_settings)
            .setMessage(R.string.reset_dolphin_settings_confirmation)
            .setPositiveButton(R.string.yes) { _, _ -> resetSettings() }
            .setNegativeButton(R.string.no, null)
            .show()
    }

    private fun resetSettings() {
        NativeLibrary.ResetDolphinSettings()
        ThemeHelper.resetThemePreferences(this, false)

        val restartIntent = Intent(this, UserDataActivity::class.java)
        restartIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)

        finish()
        overridePendingTransition(0, 0)
        startActivity(restartIntent)
        overridePendingTransition(0, 0)
    }

    private fun openFileManager() {
        // First, try to open the user data folder directly
        try {
            startActivity(getFileManagerIntentOnDocumentProvider(Intent.ACTION_VIEW))
            return
        } catch (_: ActivityNotFoundException) {}

        try {
            startActivity(getFileManagerIntentOnDocumentProvider("android.provider.action.BROWSE"))
            return
        } catch (_: ActivityNotFoundException) {}

        try {
            // Just try to open the file manager, try the package name used on "normal" phones
            startActivity(getFileManagerIntent("com.google.android.documentsui"))
            return
        } catch (_: ActivityNotFoundException) {}

        try {
            // Next, try the AOSP package name
            startActivity(getFileManagerIntent("com.android.documentsui"))
            return
        } catch (_: ActivityNotFoundException) {}

        try {
            // Activity not found. Perhaps it was removed by the OEM, or by some new Android version
            // that didn't exist at the time of writing. Not much we can do other than tell the user.
            val arguments = Bundle()
            arguments.putInt(
                NotificationDialog.KEY_MESSAGE,
                R.string.user_data_open_system_file_manager_failed
            )

            val dialog = NotificationDialog()
            dialog.arguments = arguments
            dialog.show(supportFragmentManager, NotificationDialog.TAG)
            return
        } catch (_: ActivityNotFoundException) {}
    }

    private fun getFileManagerIntent(packageName: String): Intent {
        // Fragile, but some phones don't expose the system file manager in any better way
        val intent = Intent(Intent.ACTION_MAIN)
        intent.setClassName(packageName, "com.android.documentsui.files.FilesActivity")
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        return intent
    }

    private fun getFileManagerIntentOnDocumentProvider(action: String): Intent {
        val authority = "$packageName.user"
        val intent = Intent(action)
        intent.addCategory(Intent.CATEGORY_DEFAULT)
        intent.data = DocumentsContract.buildRootUri(authority, DocumentProvider.ROOT_ID)
        intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        return intent
    }

    private fun importUserData() {
        requestImport.launch(arrayOf("application/zip"))
    }

    fun importUserData(source: Uri): Int {
        try {
            if (!isDolphinUserDataBackup(source)) {
                return R.string.user_data_import_invalid_file
            }

            taskViewModel.onResultDismiss = {
                // Restart the app to apply the imported user data.
                exitProcess(0)
            }

            contentResolver.openInputStream(source).use { `is` ->
                ZipInputStream(`is`).use { zis ->
                    val userDirectory = File(DirectoryInitialization.getUserDirectory())
                    val userDirectoryCanonicalized = userDirectory.canonicalPath + '/'

                    deleteChildrenRecursively(userDirectory)

                    DirectoryInitialization.getGameListCache(this).delete()

                    var ze: ZipEntry? = zis.nextEntry
                    val buffer = ByteArray(BUFFER_SIZE)
                    while (ze != null) {
                        val destFile = File(userDirectory, ze.name)
                        val destDirectory = if (ze.isDirectory) destFile else destFile.parentFile

                        if (!destFile.canonicalPath.startsWith(userDirectoryCanonicalized)) {
                            Log.error("Zip file attempted path traversal! " + ze.name)
                            return R.string.user_data_import_failure
                        }

                        if (!destDirectory.isDirectory && !destDirectory.mkdirs()) {
                            throw IOException("Failed to create directory $destDirectory")
                        }

                        if (!ze.isDirectory) {
                            FileOutputStream(destFile).use { fos ->
                                var count: Int
                                while (zis.read(buffer).also { count = it } != -1) {
                                    fos.write(buffer, 0, count)
                                }
                            }

                            val time = ze.time
                            if (time > 0) {
                                destFile.setLastModified(time)
                            }
                        }
                        ze = zis.nextEntry
                    }
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
            return R.string.user_data_import_failure
        } catch (e: NullPointerException) {
            e.printStackTrace()
            return R.string.user_data_import_failure
        }
        return R.string.user_data_import_success
    }

    @Throws(IOException::class)
    private fun isDolphinUserDataBackup(uri: Uri): Boolean {
        contentResolver.openInputStream(uri).use { `is` ->
            ZipInputStream(`is`).use { zis ->
                var ze: ZipEntry
                while (zis.nextEntry.also { ze = it } != null) {
                    val name = ze.name
                    if (name == "Config/Dolphin.ini") {
                        return true
                    }
                }
            }
        }
        return false
    }

    @Throws(IOException::class)
    private fun deleteChildrenRecursively(directory: File) {
        val children =
            directory.listFiles() ?: throw IOException("Could not find directory $directory")
        for (child in children) {
            deleteRecursively(child)
        }
    }

    @Throws(IOException::class)
    private fun deleteRecursively(file: File) {
        if (file.isDirectory) {
            deleteChildrenRecursively(file)
        }

        if (!file.delete()) {
            throw IOException("Failed to delete $file")
        }
    }

    private fun exportUserData() {
        requestExport.launch("dolphin-emu.zip")
    }

    private fun exportUserData(destination: Uri): Int {
        try {
            contentResolver.openOutputStream(destination).use { os ->
                ZipOutputStream(os).use { zos ->
                    exportUserData(
                        zos,
                        File(DirectoryInitialization.getUserDirectory()),
                        null
                    )
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
            return R.string.user_data_export_failure
        }
        return R.string.user_data_export_success
    }

    @Throws(IOException::class)
    private fun exportUserData(zos: ZipOutputStream, input: File, pathRelativeToRoot: File?) {
        if (input.isDirectory) {
            val children = input.listFiles() ?: throw IOException("Could not find directory $input")

            // Check if the coroutine was cancelled
            if (!taskViewModel.cancelled) {
                for (child in children) {
                    exportUserData(zos, child, File(pathRelativeToRoot, child.name))
                }
            }
            if (children.isEmpty() && pathRelativeToRoot != null) {
                zos.putNextEntry(ZipEntry(pathRelativeToRoot.path + '/'))
            }
        } else {
            FileInputStream(input).use { fis ->
                val buffer = ByteArray(BUFFER_SIZE)
                val entry = ZipEntry(pathRelativeToRoot!!.path)
                entry.time = input.lastModified()
                zos.putNextEntry(entry)
                var count: Int
                while (fis.read(buffer, 0, buffer.size).also { count = it } != -1) {
                    zos.write(buffer, 0, count)
                }
            }
        }
    }

    private fun setInsets() {
        ViewCompat.setOnApplyWindowInsetsListener(mBinding.appbarUserData) { _: View?, windowInsets: WindowInsetsCompat ->
            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())

            InsetsHelper.insetAppBar(insets, mBinding.appbarUserData)

            mBinding.scrollViewUserData.setPadding(insets.left, 0, insets.right, insets.bottom)

            InsetsHelper.applyNavbarWorkaround(insets.bottom, mBinding.workaroundView)
            windowInsets
        }
    }

    companion object {
        private const val BUFFER_SIZE = 64 * 1024

        @JvmStatic
        fun launch(context: Context) {
            val launcher = Intent(context, UserDataActivity::class.java)
            context.startActivity(launcher)
        }
    }
}
